/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

//! Functions for validating and extracting qualified XML names.

use html5ever::{namespace_url, ns, LocalName, Namespace, Prefix};

use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
use crate::dom::bindings::str::DOMString;

/// Validate a qualified name. See <https://dom.spec.whatwg.org/#validate> for details.
pub fn validate_qualified_name(qualified_name: &str) -> ErrorResult {
    // Step 2.
    match xml_name_type(qualified_name) {
        XMLName::Invalid => Err(Error::InvalidCharacter),
        XMLName::Name => Err(Error::InvalidCharacter), // see whatwg/dom#671
        XMLName::QName => Ok(()),
    }
}

/// Validate a namespace and qualified name and extract their parts.
/// See <https://dom.spec.whatwg.org/#validate-and-extract> for details.
pub fn validate_and_extract(
    namespace: Option<DOMString>,
    qualified_name: &str,
) -> Fallible<(Namespace, Option<Prefix>, LocalName)> {
    // Step 1.
    let namespace = namespace_from_domstring(namespace);

    // Step 2.
    validate_qualified_name(qualified_name)?;

    let colon = ':';

    // Step 5.
    let mut parts = qualified_name.splitn(2, colon);

    let (maybe_prefix, local_name) = {
        let maybe_prefix = parts.next();
        let maybe_local_name = parts.next();

        debug_assert!(parts.next().is_none());

        if let Some(local_name) = maybe_local_name {
            debug_assert!(!maybe_prefix.unwrap().is_empty());

            (maybe_prefix, local_name)
        } else {
            (None, maybe_prefix.unwrap())
        }
    };

    debug_assert!(!local_name.contains(colon));

    match (namespace, maybe_prefix) {
        (ns!(), Some(_)) => {
            // Step 6.
            Err(Error::Namespace)
        },
        (ref ns, Some("xml")) if ns != &ns!(xml) => {
            // Step 7.
            Err(Error::Namespace)
        },
        (ref ns, p) if ns != &ns!(xmlns) && (qualified_name == "xmlns" || p == Some("xmlns")) => {
            // Step 8.
            Err(Error::Namespace)
        },
        (ns!(xmlns), p) if qualified_name != "xmlns" && p != Some("xmlns") => {
            // Step 9.
            Err(Error::Namespace)
        },
        (ns, p) => {
            // Step 10.
            Ok((ns, p.map(Prefix::from), LocalName::from(local_name)))
        },
    }
}

/// Results of `xml_name_type`.
#[derive(PartialEq)]
#[allow(missing_docs)]
pub enum XMLName {
    QName,
    Name,
    Invalid,
}

/// Check if an element name is valid. See <http://www.w3.org/TR/xml/#NT-Name>
/// for details.
pub fn xml_name_type(name: &str) -> XMLName {
    fn is_valid_start(c: char) -> bool {
        matches!(c, ':' |
            'A'..='Z' |
            '_' |
            'a'..='z' |
            '\u{C0}'..='\u{D6}' |
            '\u{D8}'..='\u{F6}' |
            '\u{F8}'..='\u{2FF}' |
            '\u{370}'..='\u{37D}' |
            '\u{37F}'..='\u{1FFF}' |
            '\u{200C}'..='\u{200D}' |
            '\u{2070}'..='\u{218F}' |
            '\u{2C00}'..='\u{2FEF}' |
            '\u{3001}'..='\u{D7FF}' |
            '\u{F900}'..='\u{FDCF}' |
            '\u{FDF0}'..='\u{FFFD}' |
            '\u{10000}'..='\u{EFFFF}')
    }

    fn is_valid_continuation(c: char) -> bool {
        is_valid_start(c) ||
            matches!(c,
                '-' |
                '.' |
                '0'..='9' |
                '\u{B7}' |
                '\u{300}'..='\u{36F}' |
                '\u{203F}'..='\u{2040}')
    }

    let mut iter = name.chars();
    let mut non_qname_colons = false;
    let mut seen_colon = false;
    let mut last = match iter.next() {
        None => return XMLName::Invalid,
        Some(c) => {
            if !is_valid_start(c) {
                return XMLName::Invalid;
            }
            if c == ':' {
                non_qname_colons = true;
            }
            c
        },
    };

    for c in iter {
        if !is_valid_continuation(c) {
            return XMLName::Invalid;
        }
        if c == ':' {
            if seen_colon {
                non_qname_colons = true;
            } else {
                seen_colon = true;
            }
        }
        last = c
    }

    if last == ':' {
        non_qname_colons = true
    }

    if non_qname_colons {
        XMLName::Name
    } else {
        XMLName::QName
    }
}

/// Convert a possibly-null URL to a namespace.
///
/// If the URL is None, returns the empty namespace.
pub fn namespace_from_domstring(url: Option<DOMString>) -> Namespace {
    match url {
        None => ns!(),
        Some(s) => Namespace::from(s),
    }
}
